package engine;

import com.sun.xml.internal.stream.util.ReadOnlyIterator;

import java.awt.*;
import java.util.*;
import java.util.List;
import java.util.logging.Logger;

/**
 * This class describes genome, phenotype and inter biosphere of creature.
 */
public class Creature {
    private static Logger log = Logger.getLogger(Creature.class.getName());

    public static final int COEF_OF_FINDING = 10;
    public static final int COEF_OF_ENERGY_HUNGER = 20;
    public static final int TESTOSTERONE_REPRODUCTION = 60;
    public static final int HEALTH_REPRODUCTION = 20;
    public static final int HEALTH_ENERGY_COEF = 5;
    public static final int VISION_DIFF_COEF = 1000;
    public static final int SAME_TYPE_COEF = 2000;
    public static final int SAME_TYPE_MEAT_PREFER_COEF = 10;
    public static int numberOfCreatures = 0;

    public final int number;
    int[] genome;
    public Cell position;
    public Decision decision;

    private Creature() {
        number = ++numberOfCreatures;

    }

    public Creature(Cell cell) {
        this();
        position = cell;
    }


    public Creature(Cell cell, SexType sex, int genSpeed, int genFoodNeed, int genStamina, int genWidth, int meatPrefer,
                    Color color, int maxEnergy, int energyRecovery, float vision, int attack, int armor) {
        this(cell);
        this.setGenWidth(genWidth);
        this.setWidth(genWidth);
        this.setSpeed(genSpeed);
        this.setStamina(genStamina);
        this.setFoodNeed(genFoodNeed);
        this.setGenSpeed(genSpeed);
        this.setGenFoodNeed(genFoodNeed);
        this.setGenStamina(genStamina);
        this.setSex(sex);
        this.setColor(color);
        this.setMeatPrefer(meatPrefer);
        this.setMaxEnergy(maxEnergy);
        this.setEnergyRecovery(energyRecovery);
        this.setVision(vision);
        this.setAttack(attack);
        this.setArmor(armor);
        health = width;
    }

    public Creature(Cell cell, SexType sex, int genSpeed, int genFoodNeed, int genStamina, int genWidth, int meatPrefer,
                    Color color, int maxEnergy, int energyRecovery, float vision, int attack, int armor, Creature father, Creature mother) {
        this(cell, sex, genSpeed, genFoodNeed, genStamina, genWidth, meatPrefer, color, maxEnergy, energyRecovery, vision, attack, armor);
        this.father = father;
        this.mother = mother;
    }
    // ---- Current State ---

    public boolean isAlive = true;
    private int age = 0;
    private int health;
    private int hunger = 0;
    private int testosterone = 0;  // Уровень тестостерона
    private int energy = 0;    // Усталость

    // ---- Phenotype ---

    private int width;
    private int maxEnergy;
    private int speed;
    private int foodNeed;
    private int stamina;
    private int energyRecovery;
    private int attack;
    private int armor;

    // ---- Genome ---
    private Creature father;
    private Creature mother;
    private SexType sex;
    private Color color;

    private int genSpeed;        // Скорость
    private int genFoodNeed;     // Прожорливость
    private int genStamina;     // Выносливость
    private int genWidth;    //
    private int meatPrefer;  // Предпочтение к мясу
    private float vision; // Зрение

    public int getMaxEnergy() {
        return maxEnergy;
    }

    public void setMaxEnergy(int maxEnergy) {
        this.maxEnergy = maxEnergy;
    }

    public void setSpeed(int speed) {
        this.speed = speed;
    }

    public void setFoodNeed(int foodNeed) {
        this.foodNeed = foodNeed;
    }

    public void setStamina(int stamina) {
        this.stamina = stamina;
    }

    public void setVision(float vision) {
        if (vision < 0) {
            vision = 0;
        }
        if (vision > 1) {
            vision = 1;
        }
        this.vision = vision;
    }

    public int getSpeed() {

        return speed;
    }

    public int getFoodNeed() {
        return foodNeed;
    }

    public int getStamina() {
        return stamina;
    }

    public int getEnergyRecovery() {
        return energyRecovery;
    }

    public int[] getGenome() {
        return genome;
    }


    public SexType getSex() {
        return sex;
    }

    public int getAge() {
        return age;
    }

    public int getHunger() {
        return hunger;
    }

    public int getTestosterone() {
        return testosterone;
    }

    public int getWidth() {
        return width;
    }

    public float getVision() {
        return vision;
    }

    public int getEnergy() {
        return energy;
    }

    public Color getColor() {
        return color;
    }

    public int getGenSpeed() {
        return genSpeed;
    }

    public int getGenFoodNeed() {
        return genFoodNeed;
    }

    public int getGenStamina() {
        return genStamina;
    }

    public int getGenWidth() {
        return genWidth;
    }

    public int getMeatPrefer() {
        return meatPrefer;
    }

    public int getHealth() {
        return health;
    }

    public int getAttack() {
        return attack;
    }

    public int getArmor() {
        return armor;
    }

    public Creature getFather() {
        return father;
    }

    public Creature getMother() {
        return mother;
    }

    public static void setNumberOfCreatures(int numberOfCreatures) {
        Creature.numberOfCreatures = numberOfCreatures;
    }

    public void setGenome(int[] genome) {
        this.genome = genome;
    }

    public void setEnergyRecovery(int energyRecovery) {
        this.energyRecovery = energyRecovery;
    }

    public void setPosition(Cell position) {
        this.position = position;
    }

    public void setSex(SexType sex) {
        this.sex = sex;
    }

    public void increaseAge() {
        age++;
    }

    public void increaseHunger(int hungerDiff) {
        hunger = setBorder(hunger + hungerDiff, 0, 2 * width);
    }

    public void increaseTestosterone(int testosterone) {
        this.testosterone += testosterone;
        this.testosterone = setBorder(this.testosterone, 0, width);
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public void increaseEnergy(int energyDiff) {
        energy += energyDiff;
        energy = setBorder(energy, 0, maxEnergy);
    }

    public void increaseHealth(int healthDiff) {
        health += healthDiff;
        health = setBorder(health, 0, width);
    }

    public void setColor(Color color) {
        this.color = color;
    }

    public void setGenSpeed(int genSpeed) {
        genSpeed = positive(genSpeed);
        this.genSpeed = genSpeed;
    }

    public void setGenFoodNeed(int genFoodNeed) {
        genFoodNeed = positive(genFoodNeed);
        this.genFoodNeed = genFoodNeed;
    }

    public void setGenStamina(int genStamina) {
        genStamina = positive(genStamina);
        this.genStamina = genStamina;
    }

    public void setGenWidth(int genWidth) {
        genWidth = positive(genWidth);
        this.genWidth = genWidth;
    }

    public void setMeatPrefer(int meatPrefer) {
        meatPrefer = setBorder(meatPrefer, 0, 100);
        this.meatPrefer = meatPrefer;
    }

    public void setAttack(int attack) {
        this.attack = positive(attack);
    }

    public void setArmor(int armor) {
        this.armor = positive(armor);
    }

    private static int setBorder(int number, int lowBorder, int upperBorder) {
        if (upperBorder < lowBorder) {
            return number;
        }
        return (number < lowBorder) ? lowBorder : (number > upperBorder) ? upperBorder : number;
    }

    // ---- Biosphere ---


    // ---- Methods ---
    @Override
    public String toString() {
        return "" + number;
    }

    /**
     * This method form String with all meaning properties of creature.
     * Is used to get full information of creature after death.
     */
    public String allProperties() {
        StringBuilder s = new StringBuilder();
        s.append("\nAge = " + age);
        s.append("\nHealth = " + health);
        s.append("\nHunger = " + hunger);
        s.append("\nEnergy = " + energy);
        s.append("\nTestosterone = " + testosterone);
        s.append("\nWidth = " + width);
        s.append("\nSpeed = " + speed);
        s.append("\nStamina = " + stamina);
        s.append("\nFoodNeed = " + foodNeed);
        s.append("\nMeatPrefer = " + meatPrefer);
        s.append("\nAttack = " + attack);
        s.append("\nArmor = " + armor);
        s.append("\nEnergy recovery = " + energyRecovery);
        s.append("\nMax energy = " + maxEnergy);
        s.append("\nVision = " + vision);
        s.append("\nSex = " + sex);
        return s.toString();
    }

    public Decision dayDecision() {
        if (hunger >= width / 2) {
            int meat = (int) (Math.random() * 100 - meatPrefer);
            Decision dec = (meat < 0) ? Decision.hunt : Decision.eatGrass;
            if (dec == Decision.hunt &&
                    (energy < maxEnergy / 2 || health < width / 2)) {
                dec = Decision.stay;
                log.info("Creature №" + this + " decided to rest before hunting.");

            }
            return dec;
        }
        if (testosterone >= width / 2 && (sex == SexType.male || energy >= width / 2)) {
            return Decision.lookForLove;
        } else return Decision.stay;
    }


    public boolean dayAction(Decision decision) {
        boolean pleasure = true;
        this.decision = decision;
        switch (decision) {
            case stay:
                break;
            case eatGrass:
                if (position.grassEnough) {
                    position.setGrass(position.getGrass() - hunger);
                    position.myWorld.shLog.append("\nCreature №").append(this).append(" ate ").append(hunger).append(" grass.");
                    log.info("Creature №" + this + " ate " + hunger + " grass.");
                    hunger = 0;
                } else {
                    if (hunger < position.grassPortion) {
                        position.myWorld.shLog.append("\nCreature №").append(this).append(" ate ").append(hunger).append(" grass.");
                        log.info("Creature №" + this + " ate " + hunger + " grass.");
                        hunger = 0;
                        position.setGrass(position.getGrass() - hunger);
                    } else {
                        position.myWorld.shLog.append("\nCreature №" + this + " ate " + position.grassPortion + " grass.");
                        log.info("Creature №" + this + " ate " + position.grassPortion + " grass.");
                        hunger -= position.grassPortion;
                        position.setGrass(position.getGrass() - position.grassPortion);
                    }
                }
                break;
            case hunt:
                Creature victim = lookForVictim();
                if (victim == null) {
                    position.myWorld.shLog.append("\nCreature №" + this + " found no victim.");
                    log.info("Creature №" + this + " found no victim.");
                    pleasure = false;
                    break;
                }
                Hunting hunting = new Hunting(position, this, victim);
                pleasure = hunting.start();
                break;
            case lookForLove:
                Creature lover = lookForLover();
                if (lover == null) {
                    position.myWorld.shLog.append("\nCreature №" + this + " found no lover.");
                    log.info("Creature №" + this + " found no lover.");
                    pleasure = false;
                    break;
                }
                pleasure = reproduction(lover);
        }

        return pleasure;
    }

    public static Color colorConstr(int r, int g, int b) {
        return new Color(setBorder(r, 0, 255), setBorder(g, 0, 255), setBorder(b, 0, 255));
    }

    public boolean giveUp(Creature opponent, int distance, boolean iAmVictim) {
        if (iAmVictim) {
            if (looksLike(opponent.getAttack()) * 1.1 < armor) {
                position.myWorld.shLog.append("\nVictim ").append(this).append(" is not afraid of ").append(opponent);
                log.info("Victim " + this + " is not afraid of " + opponent);
                return true; // Жертва не боится нападающего
            }
            if (energy > 0) {
                return false;
            } else return true;
        }
        if (attack - looksLike(opponent.getArmor()) < (looksLike(opponent.getAttack())  - armor) * 0.8 ) {
            position.myWorld.shLog.append("\nHunter ").append(this).append(" is afraid of ").append(opponent);
            log.info("Hunter " + this + " is afraid of " + opponent);
            return true;
        }
        if (looksLike(opponent.getArmor()) * 0.9 > attack) {
            position.myWorld.shLog.append("\nHunter ").append(this).append(" can't damage ").append(opponent);
            log.info("Hunter " + this + " can't damage " + opponent);
            return true;
        }
        if (energy <= 0 ||
                (((long) distance * (speed - looksLike(opponent.getSpeed()) * 0.6) * width / stamina / Hunting.TIRED_COEF) > energy)) {
            log.info("Hunter " + this + " have not enough energy to run.");
            return true;
        }
        return false;
    }

    public boolean lookForCreature(Creature obj) {
        return lookForCreature(obj, Cell.CELL_SIZE / position.creaturesOfCell.size());
    }

    public boolean lookForCreature(Creature obj, int dist) {
        if (dist <= 0) {
            return true;
        }
        double rand = Math.random();
        double chance = 1 / (1 + vision * obj.width / dist / dist * Hunting.colorDiff(position.color, obj.color) * COEF_OF_FINDING);
        log.info("Random = " + rand + ", Chance = " + chance + ", dist = " + dist + ", result = " + (rand > chance));
        return (rand > chance);
    }

    /**
     * This method change everyday characteristics as energy, hunger and testosterone.
     *
     * @param needDouble is true, if recovery of energy should be doubled (if creature stay this day).
     */
    public void rest(boolean needDouble) {
        int earnedEnergy;
        int enRec = energyRecovery * ((needDouble) ? 2 : 1);
        int healthLack = positive(width - health);
        if (energy + enRec - healthLack / HEALTH_ENERGY_COEF <= maxEnergy) {
            earnedEnergy = enRec - healthLack / HEALTH_ENERGY_COEF;
        } else {
            earnedEnergy = maxEnergy - energy;
        }
        increaseEnergy(earnedEnergy);
        increaseHunger(foodNeed + earnedEnergy / COEF_OF_ENERGY_HUNGER);
        increaseTestosterone(width / TESTOSTERONE_REPRODUCTION);
        increaseHealth(width / HEALTH_REPRODUCTION);
    }

    /**
     * Tiredness of one run is calculated
     *
     * @return distance of one run
     */
    public int run() {
        int spendEnergy = speed * speed * width / stamina / Hunting.TIRED_COEF;
        if (spendEnergy > (energy / 2)) {
            int tiredSpeed = (int) Math.sqrt(energy * stamina * Hunting.TIRED_COEF / 2 / width);
            energy -= tiredSpeed * tiredSpeed * width / stamina / Hunting.TIRED_COEF;
            log.info("Tired speed of " + this + " = " + tiredSpeed);
            return tiredSpeed;
        }
        energy -= speed * speed * width / stamina / Hunting.TIRED_COEF;
        return speed;
    }

    public int hit(Creature target) {
        int spendEnergy = attack * width / stamina / Hunting.TIRED_COEF;
        if (energy <= spendEnergy) {
            log.info("Creature " + this + " haven't enough energy for hit.");
            return 0;
        }
        energy -= spendEnergy;
        int damage = Math.max((attack - target.armor), 0);
        target.increaseHealth(-damage);
        return damage;
    }

    private boolean reproduction(Creature lover) {
        if (lover.getSex() == this.getSex()) {
            position.myWorld.shLog.append("\nCreature №" + this + " and " + lover + " are the same sex and can't reproduce with each other.");
            log.info("Creature №" + this + " and " + lover + " are the same sex and can't reproduce with each other.");
            return false;
        }
        Creature father;
        Creature mother;
        if (lover.getSex() == SexType.male) {
            father = lover;
            mother = this;
        } else {
            father = this;
            mother = lover;
        }
        if (mother.getEnergy() <= (mother.getWidth() + father.getWidth()) / 4) {
            position.myWorld.shLog.append("\nCreature №" + mother + " have not enough energy for reproduction with " + father);
            log.info("Creature №" + mother + " have not enough energy for reproduction with " + father);
            return false;
        }
        if (lover.getTestosterone() < lover.getWidth() / 2) {
            position.myWorld.shLog.append("\nCreature №" + lover + " isn't ready for reproduction with " + this);
            log.info("Creature №" + lover + " isn't ready for reproduction with " + this);
            return false;
        }
        if (this.getTestosterone() < this.getWidth() / 2) {
            position.myWorld.shLog.append("\nCreature №" + this + " isn't ready for reproduction with " + lover);
            log.info("Creature №" + this + " isn't ready for reproduction with " + lover);
            return false;
        }

        int width = (father.width + mother.width) / 2 + random(mother.width);
        int speed = (father.speed + mother.speed) / 2 + random(mother.speed);
        int food = (father.foodNeed + mother.foodNeed) / 2 + random(mother.foodNeed);
        int stamina = (father.stamina + mother.stamina) / 2 + random(mother.stamina);
        SexType sex = ((Math.random() * 100 - 50) < 0) ? SexType.male : SexType.female;
        int maxEnergy = (father.maxEnergy + mother.maxEnergy) / 2 + random(mother.maxEnergy);
        int energyRecovery = (father.energyRecovery + mother.energyRecovery) / 2 + random(mother.energyRecovery);
        float vision = (father.vision + mother.vision) / 2 + random(mother.vision);
        int attack = (father.attack + mother.attack) / 2 + random(mother.attack);
        int armor = (father.armor + mother.armor) / 2 + random(mother.armor);
        int colorRRand = random(mother.color.getRed());
        int colorGRand = random(mother.color.getGreen());
        int colorBRand = random(mother.color.getBlue());
        Color color = Creature.colorConstr(
                (father.color.getRed() + mother.color.getRed()) / 2 + colorRRand,
                (father.color.getGreen() + mother.color.getGreen()) / 2 + colorGRand,
                (father.color.getBlue() + mother.color.getBlue()) / 2 + colorBRand);
        int meatPrefer = (father.meatPrefer + mother.meatPrefer) / 2 + random(mother.meatPrefer);
        Cell cell = this.position;

        Creature child = new Creature(cell, sex, speed, food, stamina, width, meatPrefer, color, maxEnergy, energyRecovery, vision, attack, armor, father, mother);
        position.creaturesOfCell.add(child);
        position.myWorld.creaturesOfWorld.add(child);

        this.testosterone = 0;
        lover.testosterone = 0;
        if (mother.energy < child.getWidth() / 2) {
            mother.die("child");
        } else {
            mother.increaseEnergy(-(child.getWidth() / 2));
        }
        position.myWorld.shLog.append("\nCreature №" + this + " with " + lover + " had a child " + child);
        log.info("Creature №" + this + " with " + lover + " had a child " + child);
        return true;
    }


    /**
     * Help method to make sure value is not negative.
     * In case <code>i</code> is negative, zero returns.
     *
     * @param i
     * @return not negative value
     */
    private int positive(int i) {
        return (i < 0) ? 0 : i;
    }

    public boolean sameType(Creature creature) {
        long diff = Hunting.colorDiff(color, creature.getColor());
        diff += Math.abs(width - creature.getWidth());
        diff += Math.abs(speed - creature.getSpeed());
        diff += Math.abs(stamina - creature.getStamina());
        diff += Math.abs(maxEnergy - creature.getMaxEnergy());
        diff += Math.abs(vision - creature.getVision()) * VISION_DIFF_COEF;
        diff += Math.abs(foodNeed - creature.getFoodNeed());
        diff += Math.abs(meatPrefer - creature.getMeatPrefer()) * SAME_TYPE_MEAT_PREFER_COEF;
        diff += Math.abs(attack - creature.getAttack());
        diff += Math.abs(armor - creature.getArmor());
        diff += Math.abs(energyRecovery - creature.getEnergyRecovery());
        //position.myWorld.shLog.append("\nCreature №").append(this).append(" have diff with №").append(creature).append(" = ").append(diff);
        log.info("Creature №" + this + " have diff with №" + creature + " = " + diff);
        return diff < SAME_TYPE_COEF;
    }

    private int victimRating(Creature victim) {
        int speedDiff = positive(speed - looksLike(victim.getSpeed()));
        int damage = positive(attack - looksLike(victim.getArmor()));
        int myTime = looksLike(victim.getWidth()) / (positive(attack - looksLike(victim.getArmor())) + 1);
        int hisTime = width / (positive(looksLike(victim.getAttack()) - armor) + 1);
        int timeDiff = positive(hisTime - myTime);
        int rating = speedDiff * damage * timeDiff;
        //position.myWorld.shLog.append("\nCreature №").append(this).append(" set rating ").append(rating).append(" to creature ").append(victim);
        log.info("Creature №" + this + " set rating " + rating + " to creature " + victim);
        return rating;
    }

    public Creature lookForVictim() {
        java.util.List<Creature> founds = new ArrayList<>();
        position.creaturesOfCell.stream()
                .filter(p -> !p.equals(this))
                .filter(p -> lookForCreature(p))
                .filter(p -> !sameType(p))
                .filter(p -> looksLike(p.getSpeed()) <= speed)
                .filter(p -> looksLike(p.getArmor()) < getAttack())
                .forEach(p -> founds.add(p));
        if (founds.isEmpty()) {
            return null;
        }
        if (founds.size() == 1) {
            return (victimRating(founds.get(0)) > 0) ? founds.get(0) : null;
        }
        Creature victim = founds.get(0);
        Map<Integer, Creature> ratedFounds = new HashMap<>();
        long rateSum = 0;
        int rateMin = 0;
        for (Creature curr : founds) {
            int rate = victimRating(curr);
            if (rate > 0) {
                if (rateMin <= 0) {
                    rateMin = rate;
                } else {
                    rateMin = Math.min(rate, rateMin);
                }
                rateSum += rate;
                ratedFounds.put(rate, curr);
            }
        }
        long rateDecrease = Math.min(rateMin, rateSum / founds.size() / 2);
        long random = (long) (Math.random() * (rateSum - rateDecrease * founds.size()));
        for (Map.Entry<Integer, Creature> entry : ratedFounds.entrySet()) {
            int rate = entry.getKey() - (int)rateDecrease;
            if (rate < random) {
                random -= rate;
            } else {
                victim = entry.getValue();
                break;
            }
        }
        return victim;
    }

    public Creature lookForLover() {
        List<Creature> founds = new ArrayList<>();
        position.creaturesOfCell.stream()
                .filter(p -> !p.equals(this))
                .filter(p -> p.getSex() != sex)
                .filter(p -> sameType(p))
                .filter(p -> looksLike(p.getTestosterone()) >= looksLike(p.getWidth() / 2))
                .filter(p -> looksLike(p.getEnergy()) >= looksLike(p.getMaxEnergy() / 2))
                .forEach(p -> founds.add(p));
        if (founds.isEmpty()) {
            return null;
        }
        return founds.get((int) (Math.random() * founds.size()));
    }

    public void die() {
        isAlive = false;
        log.info("Creature is dead");
        position.creaturesOfCell.remove(this);
        position.myWorld.creaturesOfWorld.remove(this);
        position.myWorld.shLog.append("\nCreature №").append(this).append(" is dead");
    }

    public void die(String reason) {
        isAlive = false;
        log.info("Creature №" + this + " is dead because of " + reason + "." + allProperties());
        position.creaturesOfCell.remove(this);
        position.myWorld.creaturesOfWorld.remove(this);
        position.myWorld.shLog.append("\nCreature №").append(this).append(" is dead because of ").append(reason);
    }

    public int looksLike(int property) {
        int random = (int) ((1 - vision) * (Math.random() - 0.5) * 2 * property);
        // TODO it should depend on distance
        return property + random;
    }

    public static int random(int dispersion) {
        return (int) (Math.random() * dispersion - dispersion / 2);
    }

    public static float random(float dispersion) {
        return (float) (Math.random() * dispersion - dispersion / 2);
    }
    // TODO Hunter after few fails can't hunt because of lack of energy.
// TODO statistics, genome, year(or some days).
}


enum Decision {stay, eatGrass, changeCell, hunt, lookForLove}